#!/usr/bin/env python36
# -*- coding: utf-8 -*-
"""
Created on July, 2018

@author: Tangrizzly
"""
import pdb

import networkx as nx
import numpy as np


# 建图
def build_graph(train_data):
    # 建立有向图
    graph = nx.DiGraph()
    for seq in train_data:
        # 遍历序列建图，权重为两个节点之间边出现的次数
        for i in range(len(seq) - 1):
            if graph.get_edge_data(seq[i], seq[i + 1]) is None:
                weight = 1
            else:
                weight = graph.get_edge_data(seq[i], seq[i + 1])['weight'] + 1
            # 添加边和权重到有向图中
            graph.add_edge(seq[i], seq[i + 1], weight=weight)
    # 更新边的权重
    for node in graph.nodes:
        # 图中的总权重和
        sum = 0
        for j, i in graph.in_edges(node):
            sum += graph.get_edge_data(j, i)['weight']
        if sum != 0:
            for j, i in graph.in_edges(i):
                # 权值标准化
                graph.add_edge(j, i, weight=graph.get_edge_data(j, i)['weight'] / sum)
    return graph


def data_masks(all_usr_pois, item_tail):
    # 用户会话序列长度
    us_lens = [len(upois) for upois in all_usr_pois]
    # 用户会话的最大最大长度
    len_max = max(us_lens)
    # 填充序列，序列长度不足，则补0，0表示项目不存在，因为项目是从1开始的
    us_pois = [upois + item_tail * (len_max - le) for upois, le in zip(all_usr_pois, us_lens)]
    # 会话掩码？真实出现的序列项目标志为1，补充的0表示为0，填充后的session，item id 换为 1，填充位为0
    us_msks = [[1] * le + [0] * (len_max - le) for le in us_lens]
    # ======= 观察内容 =======
    # print('填充序列之后的会话.....')
    # print(us_pois[:5])
    # print('会话掩码。。。。')
    # print(us_msks[:5])
    # print('序列最大长度。。。。')
    # print(len_max)
    # ======= 观察内容 =======
    # 返回用户填充序列之后的会话、会话掩码、最大会话长度（会话填充后，长度一致）
    return us_pois, us_msks, len_max


# 从训练集中划分出验证集
def split_validation(train_set, valid_portion):
    train_set_x, train_set_y = train_set
    n_samples = len(train_set_x)
    sidx = np.arange(n_samples, dtype='int32')
    np.random.shuffle(sidx)
    n_train = int(np.round(n_samples * (1. - valid_portion)))
    valid_set_x = [train_set_x[s] for s in sidx[n_train:]]
    valid_set_y = [train_set_y[s] for s in sidx[n_train:]]
    train_set_x = [train_set_x[s] for s in sidx[:n_train]]
    train_set_y = [train_set_y[s] for s in sidx[:n_train]]
    # （序列中的项目，标签）
    return (train_set_x, train_set_y), (valid_set_x, valid_set_y)


class Data():
    def __init__(self, data, shuffle=False, graph=None):
        # ======= 观察内容 =======
        # print('datasets[0]内容是什么。。。。。。')
        # print(datasets[0])
        # print('datasets[0]的形状', np.array(datasets[0]).shape)
        # print('datasets[1]内容是什么。。。。。。')
        # print(datasets[1])
        # print('datasets[1]的形状', np.array(datasets[1]).shape)
        # ======= 观察内容 =======
        # 二元组的第0列元素，会话序列
        inputs = data[0]
        inputs, mask, len_max = data_masks(inputs, [0])
        self.inputs = np.asarray(inputs)
        self.mask = np.asarray(mask)
        self.len_max = len_max
        # 二元组的第1列元素，预测目标
        self.targets = np.asarray(data[1])
        # 输入的会话个数
        self.length = len(inputs)
        # 是否随机打乱
        self.shuffle = shuffle
        self.graph = graph

    def generate_batch(self, batch_size):
        # 将数据进行随机打乱
        if self.shuffle:
            shuffled_arg = np.arange(self.length)
            np.random.shuffle(shuffled_arg)
            self.inputs = self.inputs[shuffled_arg]
            self.mask = self.mask[shuffled_arg]
            self.targets = self.targets[shuffled_arg]
        # 划分batch，batch个数=总的会话数量/batch大小
        n_batch = int(self.length / batch_size)
        # batch个数向上取整
        if self.length % batch_size != 0:
            n_batch += 1

        slices = np.split(np.arange(n_batch * batch_size), n_batch)
        # 最后一个 batch 的会话个数为划分 (n_batch-1) 之后的剩余会话个数
        slices[-1] = slices[-1][:(self.length - batch_size * (n_batch - 1))]
        # print('slices内容。。。。。。')
        # print(slices[:5])
        return slices

    def get_slice(self, i):
        """ 获得第i个batch中的元素

        :i 对应 generate_batch 函数中生成的slices中元素的下标，表示第i个batch中的元素
        :return 返回值是这个batch内多个session对应的数据，例如A，首先要获得对应的用户的会话，然后再获得该会话对应的邻接矩阵
        """
        print('get_slice starting........')
        # 此时的inputs、msk、targets为第i个batch中对应的相应元素
        inputs, mask, targets = self.inputs[i], self.mask[i], self.targets[i]

        # ======= 内容观察 =======
        # print('get_slice里面的inputs内容。。。。。。')
        # print(inputs[:10])
        # print('get_slice里面的mask内容。。。。。。')
        # print(mask[:10])
        # print('get_slice里面的targets内容。。。。。。')
        # print(targets[:10])
        # ======= 内容观察 =======

        # items: 存储去重排序并补全后的session，和alias_input搭配使用查找A中的内容,
        # items的语义是每个session的物品空间，并不考虑时效性，所以是排序去重补全后的序列，我们用它来生成物品的embedding。
        # n_node: 会话中非重复节点个数（项目个数）
        # A: session对应的出入度矩阵，经过归一化处理；这里A是多个用户的Session出入度矩阵
        # alias_inputs: session中物品对应于去重排序后的 session 的 item集合
        items, n_node, A, alias_inputs = [], [], [], []
        # 找到用户的最大会话长度（不包含重复元素）
        for u_input in inputs:
            n_node.append(len(np.unique(u_input)))
        # 最大会话长度
        max_n_node = np.max(n_node)
        for u_input in inputs:
            # 命令行断点调试
            # pdb.set_trace()
            # 非重复节点集合，默认升序
            node = np.unique(u_input)
            # print('get_slice的u_input的唯一node集合。。。。。')
            # print(node)
            # 存储去重、排序并补全后的session
            items.append(node.tolist() + (max_n_node - len(node)) * [0])
            # 矩阵初始化
            u_A = np.zeros((max_n_node, max_n_node))
            for i in np.arange(len(u_input) - 1):
                if u_input[i + 1] == 0:
                    break
                # 找到两个相邻节点对应的位置，np.where返回的是一个tuple类型，np.where返回值解释：https://blog.csdn.net/yuezhilanyi/article/details/78918113
                u = np.where(node == u_input[i])[0][0]
                v = np.where(node == u_input[i + 1])[0][0]
                # 相邻节点之间的边设置为1
                u_A[u][v] = 1
            # 节点的入度和
            u_sum_in = np.sum(u_A, 0)
            # 入度和为0的节点，节点和设置为1，便于下面对边的权重进行标准化
            u_sum_in[np.where(u_sum_in == 0)] = 1
            # 用户会话图中节点入度标准化
            u_A_in = np.divide(u_A, u_sum_in)
            # 节点的出度和
            u_sum_out = np.sum(u_A, 1)
            u_sum_out[np.where(u_sum_out == 0)] = 1
            # # 用户会话图中节点出度标准化，为什么需要转置？ 入度矩阵的A_in(i,j)表示节点j到节点i的边权重；出度矩阵A_out(i,j)表示节点i到节点j边权重。所以需要转置
            u_A_out = np.divide(u_A.transpose(), u_sum_out)
            # 出入度计算标准化之后的邻接矩阵，为什么需要转置？ 转置的原因同上
            u_A = np.concatenate([u_A_in, u_A_out]).transpose()

            # ======= 断点调试 ======
            # 命令行断点调试，查看u_A_in和u_A
            # pdb.set_trace()
            # ======= 断点调试 ======

            # 将当前会话的邻接矩阵添加到总的邻接矩阵集合中
            A.append(u_A)
            # 记录会话序列中所有item在node中对应的位置
            alias_inputs.append([np.where(node == i)[0][0] for i in u_input])
        return alias_inputs, A, items, mask, targets
